/*
 * 文件下载
 * @Description: 这里是公共的下载接口。若有特殊下载可在其他controller中单独写
 * @Autor: HuiSir<273250950@qq.com>
 * @Date: 2020-11-28 12:39:40
 * @LastEditTime: 2022-08-02 11:22:49
 */

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import archiver from 'archiver' //文件打包
import {
    useResponseError,
    useClassDecorators,
    useMethodDecorators,
    useParameterDecorators,
    type KoaControl
} from 'koa-control'
import fileModel from '../models/File.js'
import { AccessDto, DownloadZipDto } from './dto/index.js'

const { e404 } = useResponseError()
const { Controller } = useClassDecorators()
const { Get, Post, Bind } = useMethodDecorators()
const { Body, Params, Response } = useParameterDecorators()

@Controller('/download')
export default class Download {
    /**
     * 下载单个文件（指定静态资源文件夹中的文件）
     * @description: 这个接口是否验证token，看是否放到白名单。若放到白名单，前端可以直接访问路径下载，无需ajax
     * @param {bigint} id 文件id
     * @return {*}
     * @author: HuiSir
     */
    @Get('/:id')
    @Bind(AccessDto)
    async downloadOne(
        @Params('id') id: string,
        @Response() Response: KoaControl.Response
    ): Promise<fs.ReadStream> {
        const file: KoaControl.File = await fileModel.findOne({ id: Number(id) }, '-state')

        //判断文件是否存在，不存在直接返回
        if (!fs.existsSync(file.path)) {
            e404()
            return
        }

        //判断路径是否为文件，若为文件夹，则返回
        const stat = fs.lstatSync(file.path)
        if (stat.isDirectory()) {
            e404()
            return
        }

        //此方法修改相应头的`Content-Disposition`为`attachment`类型，意为附件下载
        Response.attachment(file.path)

        //以流的形式返回前端
        return fs.createReadStream(file.path)
    }

    /**
     * @description: 通过id打包下载文件（由于打包涉及到文件操作，这里不能为白名单，必须验证token）
     * @param {string} ids 逗号分隔id,这里可能打包的文件较多，由于get请求query长度限制，所以要用post方法
     * @return {*}
     * @author: HuiSir
     */
    @Post('/zip')
    @Bind(DownloadZipDto)
    async downloadZip(@Body('ids') ids: string, @Response() Response: KoaControl.Response): Promise<fs.ReadStream> {
        // page:-1 不分页查所有
        const files: KoaControl.File[] = await fileModel.find({
            id: ids.split(',').map(item => Number(item))
        }, { page: -1 })

        // 打包文件临时路径
        const ziptmp = path.join(os.tmpdir(), `${Date.now()}.zip`)

        //创建一最终打包文件的输出流
        const zipWriteStream = fs.createWriteStream(ziptmp)

        //生成archiver对象，打包类型为zip
        const zipArchiver = archiver('zip')

        //将打包对象关联写入流（这里类同与读取流的pipe方法，虽然暂时没有文件，但会创建空的zip包）
        zipArchiver.pipe(zipWriteStream)

        //将被打包文件的流依次添加进archiver对象中
        files.forEach((file) => {
            //判断路径是否存在
            if (fs.existsSync(file.path)) {
                //判断路径是否为文件夹，若为文件夹，则整体打包
                const stat = fs.lstatSync(file.path)
                if (stat.isDirectory()) {
                    zipArchiver.directory(file.path, false) //false不打包路径
                } else {
                    zipArchiver.append(fs.createReadStream(file.path), {
                        name: file.path, //必传项，包内文件路径
                    })
                }
            }
        })

        //确认打包
        //promise方法，需要等待打包完成,否则返回前台的是空包
        await zipArchiver.finalize()

        //修改响应头为附件下载
        Response.attachment(ziptmp)

        //转发前台，由于流文件是异步打包，需要等待全部打包完毕,并不是边打包边下载
        return fs.createReadStream(ziptmp).on('close', () => {
            //下载完后删除文件(这里只有等待下载完成才会执行删除，无需再监听)
            fs.unlinkSync(ziptmp)
        })
    }
}